package com.wordplat.ikvstockchart.detector;

import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.eventhandler.InnerEvent;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.multimodalinput.event.MmiPoint;
import ohos.multimodalinput.event.TouchEvent;

/**
 * GestureDetector
 *
 * @since 2021-05-09
 */
public class GestureDetector {
    /**
     * HiLogLabel
     */
    static final HiLogLabel LABEL = new HiLogLabel(HiLog.LOG_APP, 0x00201, "GestureDetector");

    /**
     * GestureDetector
     *
     * @since 2021-05-09
     */
    public interface OnGestureListener {
        /**
         * onDown
         *
         * @param e
         * @return boolean
         */
        boolean onDown(TouchEvent e);

        /**
         * onShowPress
         *
         * @param e
         */
        void onShowPress(TouchEvent e);

        /**
         * onSingleTapUp
         *
         * @param e
         * @return boolean
         */
        boolean onSingleTapUp(TouchEvent e);

        /**
         * onScroll
         *
         * @param e1
         * @param e2
         * @param distanceX
         * @param distanceY
         * @return boolean
         */
        boolean onScroll(TouchEvent e1, TouchEvent e2, float distanceX, float distanceY);

        /**
         * onLongPress
         *
         * @param e
         */
        void onLongPress(TouchEvent e);

        /**
         * onFling
         *
         * @param e1
         * @param e2
         * @param velocityX
         * @param velocityY
         * @param scrollY0
         * @param scrollY1
         * @return boolean
         */
        boolean onFling(TouchEvent e1, TouchEvent e2, float velocityX, float velocityY, float scrollY0, float scrollY1);
    }

    /**
     * OnDoubleTapListener
     *
     * @since 2021-05-09
     */
    public interface OnDoubleTapListener {
        /**
         * onSingleTapConfirmed
         *
         * @param e
         * @return boolean
         */
        boolean onSingleTapConfirmed(TouchEvent e);

        /**
         * onDoubleTap
         *
         * @param e
         * @return boolean
         */
        boolean onDoubleTap(TouchEvent e);

        /**
         * onDoubleTapEvent
         *
         * @param e
         * @return boolean
         */
        boolean onDoubleTapEvent(TouchEvent e);
    }

    /**
     * OnContextClickListener
     *
     * @since 2021-05-09
     */
    public interface OnContextClickListener {
        /**
         * onContextClick
         *
         * @param e
         * @return boolean
         */
        boolean onContextClick(TouchEvent e);
    }

    /**
     * SimpleOnGestureListener
     *
     * @since 2021-05-09
     */
    public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener,
            OnContextClickListener {
        /**
         * onSingleTapUp
         *
         * @param e
         * @return boolean
         */
        public boolean onSingleTapUp(TouchEvent e) {
            return false;
        }

        /**
         * onLongPress
         *
         * @param e
         */
        public void onLongPress(TouchEvent e) {
        }

        /**
         * onScroll
         *
         * @param e1
         * @param e2
         * @param distanceX
         * @param distanceY
         * @return boolean
         */
        public boolean onScroll(TouchEvent e1, TouchEvent e2,
                                float distanceX, float distanceY) {
            return false;
        }

        /**
         * onFling
         *
         * @param e1        e1
         * @param e2        e2
         * @param velocityX velocityX
         * @param velocityY velocityY
         * @param scrollY0  scrollY0
         * @param scrollY1  scrollY1
         * @return boolean
         */
        public boolean onFling(TouchEvent e1, TouchEvent e2, float velocityX,
                               float velocityY, float scrollY0, float scrollY1) {
            return false;
        }

        /**
         * onShowPress
         *
         * @param e
         */
        public void onShowPress(TouchEvent e) {
        }

        /**
         * onDown
         *
         * @param e
         * @return boolean
         */
        public boolean onDown(TouchEvent e) {
            return false;
        }

        /**
         * onDoubleTap
         *
         * @param e
         * @return boolean
         */
        public boolean onDoubleTap(TouchEvent e) {
            return false;
        }

        /**
         * onDoubleTapEvent
         *
         * @param e
         * @return boolean
         */
        public boolean onDoubleTapEvent(TouchEvent e) {
            return false;
        }

        /**
         * onSingleTapConfirmed
         *
         * @param e
         * @return boolean
         */
        public boolean onSingleTapConfirmed(TouchEvent e) {
            return false;
        }

        /**
         * onContextClick
         *
         * @param e
         * @return boolean
         */
        public boolean onContextClick(TouchEvent e) {
            return false;
        }
    }

    private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
    private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
    private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout();
    private static final int DOUBLE_TAP_MIN_TIME = ViewConfiguration.getDoubleTapMinTime();

    // constants for Message.what used by GestureHandler below
    private static final int SHOW_PRESS = 1;
    private static final int LONG_PRESS = 2;
    private static final int TAP = 3;
    private int mTouchSlopSquare;
    private int mDoubleTapTouchSlopSquare;
    private int mDoubleTapSlopSquare;
    private int mMinimumFlingVelocity;
    private int mMaximumFlingVelocity;


    private final EventHandler mHandler;
    private final OnGestureListener mListener;
    private OnDoubleTapListener mDoubleTapListener;

    private boolean mStillDown;
    private boolean mDeferConfirmSingleTap;
    private boolean mInLongPress;
    private boolean mInContextClick;
    private boolean mAlwaysInTapRegion;
    private boolean mAlwaysInBiggerTapRegion;
    private boolean mIgnoreNextUpEvent;

    private TouchEvent mCurrentDownEvent;
    private TouchEvent mPreviousUpEvent;
    private long mPreviousUpOccuredTime;

    private boolean mIsDoubleTapping;

    private float mLastFocusX;
    private float mLastFocusY;
    private float mDownFocusX;
    private float mDownFocusY;

    private boolean mIsLongpressEnabled;

    private VelocityTracker mVelocityTracker;

    private class GestureHandler extends EventHandler {
        GestureHandler() {
            super(EventRunner.getMainEventRunner());
        }

        GestureHandler(EventHandler handler) {
            super(handler.getEventRunner());
        }

        @Override
        protected void processEvent(InnerEvent event) {
            switch (event.eventId) {
                case SHOW_PRESS:
                    mListener.onShowPress(mCurrentDownEvent);
                    break;

                case LONG_PRESS:
                    dispatchLongPress();
                    break;

                case TAP:
                    // If the user's finger is still down, do not count it as a tap
                    if (mDoubleTapListener != null) {
                        if (!mStillDown) {
                            mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent);
                        } else {
                            mDeferConfirmSingleTap = true;
                        }
                    }
                    break;

                default:
                    throw new RuntimeException("Unknown message " + event); //never
            }
        }
    }

    /**
     * GestureDetector
     *
     * @param listener
     * @param handler
     */
    public GestureDetector(OnGestureListener listener, EventHandler handler) {
        this(null, listener, handler);
    }

    /**
     * GestureDetector
     *
     * @param listener
     */
    public GestureDetector(OnGestureListener listener) {
        this(null, listener, null);
    }

    /**
     * GestureDetector
     *
     * @param context
     * @param listener
     */
    public GestureDetector(Context context, OnGestureListener listener) {
        this(context, listener, null);
    }

    /**
     * GestureDetector
     *
     * @param context
     * @param listener
     * @param handler
     */
    public GestureDetector(Context context, OnGestureListener listener, EventHandler handler) {
        if (handler != null) {
            mHandler = new GestureHandler(handler);
        } else {
            mHandler = new GestureHandler();
        }
        mListener = listener;
        if (listener instanceof OnDoubleTapListener) {
            setOnDoubleTapListener((OnDoubleTapListener) listener);
        }
        if (listener instanceof OnContextClickListener) {
            setContextClickListener();
        }
        init(context);
    }

    /**
     * GestureDetector
     *
     * @param context
     * @param listener
     * @param handler
     * @param unused
     */
    public GestureDetector(Context context, OnGestureListener listener, EventHandler handler,
                           boolean unused) {
        this(context, listener, handler);
    }

    private void init(Context context) {
        if (mListener == null) {
            throw new NullPointerException("OnGestureListener must not be null");
        }
        mIsLongpressEnabled = true;

        // Fallback to support pre-donuts releases
        int touchSlop, doubleTapSlop, doubleTapTouchSlop;
        //noinspection deprecation
        touchSlop = ViewConfiguration.getTouchSlop();
        doubleTapTouchSlop = touchSlop; // Hack rather than adding a hiden method for this
        doubleTapSlop = ViewConfiguration.getDoubleTapSlop();
        //noinspection deprecation
        mMinimumFlingVelocity = ViewConfiguration.getMinimumFlingVelocity();
        mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();

        mTouchSlopSquare = touchSlop * touchSlop;
        mDoubleTapTouchSlopSquare = doubleTapTouchSlop * doubleTapTouchSlop;
        mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop;
    }

    /**
     * setOnDoubleTapListener
     *
     * @param onDoubleTapListener
     */
    public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) {
        mDoubleTapListener = onDoubleTapListener;
    }

    /**
     * setContextClickListener
     */
    public void setContextClickListener() {
    }

    /**
     * setIsLongpressEnabled
     *
     * @param isLongpressEnabled
     */
    public void setIsLongpressEnabled(boolean isLongpressEnabled) {
        mIsLongpressEnabled = isLongpressEnabled;
    }

    /**
     * isLongpressEnabled
     *
     * @return isLongpressEnabled
     */
    public boolean isLongpressEnabled() {
        return mIsLongpressEnabled;
    }

    /**
     * onTouchEvent
     *
     * @param ev
     * @return boolean
     */
    public boolean onTouchEvent(TouchEvent ev) {

        if (ev == null) {
            return false;
        }
        final int action = ev.getAction();

        if (mVelocityTracker == null) {
            mVelocityTracker = new VelocityTracker();
        }
        mVelocityTracker.addMovement(ev);

        final boolean pointerUp = action == TouchEvent.OTHER_POINT_UP;
        final int skipIndex = pointerUp ? ev.getIndex() : -1;
        final boolean isGeneratedGesture = false;

        // Determine focal point
        float sumX = 0, sumY = 0;
        final int count = ev.getPointerCount();
        for (int i = 0; i < count; i++) {
            if (skipIndex == i) {
                continue;
            }
            MmiPoint point = ev.getPointerPosition(i);
            sumX += point.getX();
            sumY += point.getY();
        }
        final int div = pointerUp ? count - 1 : count;
        final float focusX = sumX / div;
        final float focusY = sumY / div;

        boolean handled = false;

        switch (action) {
            case TouchEvent.OTHER_POINT_DOWN:
                mDownFocusX = mLastFocusX = focusX;
                mDownFocusY = mLastFocusY = focusY;
                // Cancel long press and taps
                cancelTaps();
                break;

            case TouchEvent.OTHER_POINT_UP:
                mDownFocusX = mLastFocusX = focusX;
                mDownFocusY = mLastFocusY = focusY;

                // Check the dot product of current velocities.
                // If the pointer that left was opposing another velocity vector, clear.
                mVelocityTracker.calculateCurrentVelocity(1000, mMaximumFlingVelocity);
                final int upIndex = ev.getIndex();
                final int id1 = ev.getPointerId(upIndex);
                final float x1 = mVelocityTracker.getXVelocity(id1);
                final float y1 = mVelocityTracker.getYVelocity(id1);
                for (int i = 0; i < count; i++) {
                    if (i == upIndex) {
                        continue;
                    }

                    final int id2 = ev.getPointerId(i);
                    final float x = x1 * mVelocityTracker.getXVelocity(id2);
                    final float y = y1 * mVelocityTracker.getYVelocity(id2);

                    final float dot = x + y;
                    if (dot < 0) {
                        mVelocityTracker.clear();
                        break;
                    }
                }

                break;

            case TouchEvent.PRIMARY_POINT_DOWN:
                if (mDoubleTapListener != null ) {
                    boolean hadTapMessage = mHandler.hasInnerEvent(TAP);
                    if (hadTapMessage) mHandler.removeEvent(TAP);
                    if ((ev != null) && (mPreviousUpEvent != null)
                            && hadTapMessage
                            && isConsideredDoubleTap(ev, mPreviousUpEvent, ev)) {
                        // This is a second tap
                        mIsDoubleTapping = true;
                        // Give a callback with the first tap of the double-tap
                        handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent);
                        // Give a callback with down event of the double-tap
                        handled |= mDoubleTapListener.onDoubleTapEvent(ev);
                    } else {
                        // This is a first tap
                        // EventHandler 的hasInnerEvent只能通过param过滤 不能通过eventId过滤 所以得设置param
                        InnerEvent tapEvent = InnerEvent.get(TAP, TAP);
                        mHandler.sendEvent(tapEvent, DOUBLE_TAP_TIMEOUT, EventHandler.Priority.IMMEDIATE);
                    }
                }

                mDownFocusX = mLastFocusX = focusX;
                mDownFocusY = mLastFocusY = focusY;

                mCurrentDownEvent = ev;
                mAlwaysInTapRegion = true;
                mAlwaysInBiggerTapRegion = true;
                mStillDown = true;
                mInLongPress = false;
                mDeferConfirmSingleTap = false;

                if (mIsLongpressEnabled) {
                    mHandler.removeEvent(LONG_PRESS);
                    mHandler.sendTimingEvent(LONG_PRESS, mCurrentDownEvent.getStartTime() + LONGPRESS_TIMEOUT, EventHandler.Priority.IMMEDIATE);
                }
                mHandler.sendTimingEvent(SHOW_PRESS,
                        mCurrentDownEvent.getStartTime() + TAP_TIMEOUT, EventHandler.Priority.IMMEDIATE);
                handled |= mListener.onDown(ev);
                break;

            case TouchEvent.POINT_MOVE:
                if (mInLongPress || mInContextClick) {
                    break;
                }
                final float scrollX = mLastFocusX - focusX;
                final float scrollY = mLastFocusY - focusY;
                if (mIsDoubleTapping) {
                    // Give the move events of the double-tap
                    handled |= mDoubleTapListener.onDoubleTapEvent(ev);
                } else if (mAlwaysInTapRegion) {
                    final int deltaX = (int) (focusX - mDownFocusX);
                    final int deltaY = (int) (focusY - mDownFocusY);
                    int distance = (deltaX * deltaX) + (deltaY * deltaY);
                    int slopSquare = isGeneratedGesture ? 0 : mTouchSlopSquare;
                    if (distance > slopSquare) {
                        handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                        mLastFocusX = focusX;
                        mLastFocusY = focusY;
                        mAlwaysInTapRegion = false;
                        mHandler.removeEvent(TAP);
                        mHandler.removeEvent(SHOW_PRESS);
                        mHandler.removeEvent(LONG_PRESS);
                    }
                    int doubleTapSlopSquare = isGeneratedGesture ? 0 : mDoubleTapTouchSlopSquare;
                    if (distance > doubleTapSlopSquare) {
                        mAlwaysInBiggerTapRegion = false;
                    }
                } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
                    handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
                    mLastFocusX = focusX;
                    mLastFocusY = focusY;
                }
                break;

            case TouchEvent.PRIMARY_POINT_UP:
                mStillDown = false;

                TouchEvent currentUpEvent = ev;
                if (mIsDoubleTapping) {
                    handled |= mDoubleTapListener.onDoubleTapEvent(ev);
                } else if (mInLongPress) {
                    mHandler.removeEvent(TAP);
                    mInLongPress = false;
                } else if (mAlwaysInTapRegion && !mIgnoreNextUpEvent) {
                    handled = mListener.onSingleTapUp(ev);
                    if (mDeferConfirmSingleTap && mDoubleTapListener != null) {
                        mDoubleTapListener.onSingleTapConfirmed(ev);
                    }
                } else if (!mIgnoreNextUpEvent) {

                    final VelocityTracker velocityTracker = mVelocityTracker;
                    final int pointerId = ev.getPointerId(0);
                    velocityTracker.calculateCurrentVelocity(1000, mMaximumFlingVelocity);
                    final float velocityY = velocityTracker.getYVelocity(pointerId);
                    final float velocityX = velocityTracker.getXVelocity(pointerId);

                    if ((Math.abs(velocityY) > mMinimumFlingVelocity)
                            || (Math.abs(velocityX) > mMinimumFlingVelocity)) {
                        handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY, mDownFocusY, mLastFocusY);
                    }
                }
                mPreviousUpEvent = currentUpEvent;
                mPreviousUpOccuredTime = currentUpEvent.getOccurredTime();
                if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
                mIsDoubleTapping = false;
                mDeferConfirmSingleTap = false;
                mIgnoreNextUpEvent = false;
                mHandler.removeEvent(SHOW_PRESS);
                mHandler.removeEvent(LONG_PRESS);
                break;

            case TouchEvent.CANCEL:
                cancel();
                break;
        }
        return handled;
    }

    private void cancel() {
        mHandler.removeEvent(SHOW_PRESS);
        mHandler.removeEvent(LONG_PRESS);
        mHandler.removeEvent(TAP);
        mVelocityTracker.recycle();
        mVelocityTracker = null;
        mIsDoubleTapping = false;
        mStillDown = false;
        mAlwaysInTapRegion = false;
        mAlwaysInBiggerTapRegion = false;
        mDeferConfirmSingleTap = false;
        mInLongPress = false;
        mInContextClick = false;
        mIgnoreNextUpEvent = false;
    }

    private void cancelTaps() {
        mHandler.removeEvent(SHOW_PRESS);
        mHandler.removeEvent(LONG_PRESS);
        mHandler.removeEvent(TAP);
        mIsDoubleTapping = false;
        mAlwaysInTapRegion = false;
        mAlwaysInBiggerTapRegion = false;
        mDeferConfirmSingleTap = false;
        mInLongPress = false;
        mInContextClick = false;
        mIgnoreNextUpEvent = false;
    }

    private boolean isConsideredDoubleTap(TouchEvent firstDown, TouchEvent firstUp,
                                          TouchEvent secondDown) {
        if (!mAlwaysInBiggerTapRegion) {
            return false;
        }

        final long deltaTime = secondDown.getOccurredTime() - mPreviousUpOccuredTime;
        if (deltaTime > DOUBLE_TAP_TIMEOUT || deltaTime < DOUBLE_TAP_MIN_TIME) {
            return false;
        }

        MmiPoint firstDownPoint = firstDown.getPointerPosition(firstDown.getIndex());
        MmiPoint secondDownPoint = secondDown.getPointerPosition(secondDown.getIndex());
        int deltaX = (int) firstDownPoint.getX() - (int) secondDownPoint.getX();
        int deltaY = (int) firstDownPoint.getY() - (int) secondDownPoint.getY();
        int slopSquare = mDoubleTapSlopSquare;
        return (deltaX * deltaX + deltaY * deltaY < slopSquare);
    }

    private void dispatchLongPress() {
        mHandler.removeEvent(TAP);
        mDeferConfirmSingleTap = false;
        mInLongPress = true;
        mListener.onLongPress(mCurrentDownEvent);
    }
}
